/**
 *    Copyright 2012 Geensoft S.A.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package ar.com.greensoft.commons.dao.jpa;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import javax.persistence.TransactionRequiredException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import ar.com.greensoft.commons.dao.DaoException;
import ar.com.greensoft.commons.dao.GenericDao;
import ar.com.greensoft.commons.search.SearchFilter;


/**
 * Abstract JPA Data Access Object Pattern Implementation
 * @author Jorge E. Villaverde
 * @version 0.0.1
 * @since 0.0.1
 */
public abstract class AbstractJpaDaoImpl<T, PK extends Serializable> 
	implements GenericDao<T, PK> {

	@PersistenceContext
	private EntityManager entityManager;
	
	@Inject
	protected Logger logger;
	
	public T merge(T persistentObject) throws DaoException {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Merging Entity: " + persistentObject);
		try {
			T mergeEntity = entityManager.merge(persistentObject);
			entityManager.flush();
			return mergeEntity;
		} catch (PersistenceException e){
			logger.warning(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.severe(e.getMessage());
		}
		return null;
	}

	public void persist(T transientObject) throws DaoException {
		try {
			entityManager.persist(transientObject);
			entityManager.flush();
		} catch (PersistenceException e){
			logger.warning(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.severe(e.getMessage());
		}
	}

	public void refresh(T persistentObject) throws DaoException {
		entityManager.refresh(persistentObject);
	}

	public void remove(T persistentObject) throws DaoException {
		try {
			T entity = entityManager.merge(persistentObject);
			entityManager.remove(entity);
			entityManager.flush();
		} catch (IllegalArgumentException e) {
			logger.warning(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (TransactionRequiredException e) {
			logger.warning(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.severe(e.getMessage());
		}
	}
	
	protected EntityManager getEntityManager(){
		return entityManager;
	}
	
	public long count() {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Counting Entities in the Repository");
		return (Long)entityManager.createNamedQuery(getCountNamedQuery()).getSingleResult();
	}
	
	@Override
	public List<T> findEntries(int startPosition, int maxResult) throws DaoException {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Returning the list of Entities from: " + startPosition + " to " + (startPosition+maxResult));
		return createListEntityTypedQuery().setFirstResult(startPosition).setMaxResults(maxResult).getResultList();
	}
	
	@Override
	public T findById(PK id) throws DaoException {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Returning entity of type: " + getEntityClazz().getSimpleName() + "\twith id: " + id);
		return getEntityManager().find(getEntityClazz(), id);
	}	
	
	@Override
	public List<T> findAll() throws DaoException {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Returning the list of " + getEntityClazz().getSimpleName());
		TypedQuery<T> query = createListEntityTypedQuery();		
		return query.getResultList();
	}
	
	protected TypedQuery<T> createListEntityTypedQuery(){
		return getEntityManager().createNamedQuery(getListNamedQuery(), getEntityClazz());
	}
	
	protected Collection<T> findByCriteria(CriteriaQuery<T> criteria) {
		TypedQuery<T> typedQuery = entityManager.createQuery(criteria);
		return typedQuery.getResultList();
	}	

	@Override
	public List<T> findEntriesBySearchFilter(SearchFilter searchFilter, int startPosition, int maxResult) {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Returning the list of Entities from: " + startPosition + " to " + (startPosition+maxResult) + " by SeachInput");

		Class<T> entityClazz = getEntityClazz();		
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClazz);
		Root<T> root = criteriaQuery.from(entityClazz);
		
		criteriaQuery.select(root);
		Expression<Boolean> predicate = createWhereFromSearchFilter(searchFilter, root);
		if(predicate != null)
			criteriaQuery.where(predicate);
		
		List<Order> orders = createOrders(root);
		if(orders != null && !orders.isEmpty()){
			criteriaQuery.orderBy(orders);			
		}
		
		TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
		typedQuery.setFirstResult(startPosition);
		typedQuery.setMaxResults(maxResult);
		
		return typedQuery.getResultList();
	}

	@Override
	public long countBySearchFilter(SearchFilter searchFilter) {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<Long> countCriteria = criteriaBuilder.createQuery(Long.class);
		Root<T> root = countCriteria.from(getEntityClazz());
		
		countCriteria.select(criteriaBuilder.count(root));
		
		Expression<Boolean> restriction  = createWhereFromSearchFilter(searchFilter, root);
		if(restriction != null){
			countCriteria.where(restriction);
		}
		
		TypedQuery<Long> typedQuery = entityManager.createQuery(countCriteria);
		return typedQuery.getSingleResult();
	}	
	
	@Override
	public T findByName(String name) {
		if(logger.isLoggable(Level.FINE))
			logger.fine("Returning entity of type: " + getEntityClazz().getSimpleName() + "\twith name: " + name);
		entityManager.clear();
		TypedQuery<T> query = createFindByNameEntityTypedQuery();
		
		query.setParameter("name", name);
		
		return findSingleResult(query);
	}
	

	protected TypedQuery<T> createFindByNameEntityTypedQuery(){
		return getEntityManager().createNamedQuery(getFindByNameNamedQuery(), getEntityClazz());
	}
	
	protected List<Order> createOrders(Root<T> root){
		return null;
	}
	
	protected T findSingleResult(TypedQuery<T> query){
		try {
			return query.getSingleResult();
		} catch (NoResultException e) {
		} catch (NonUniqueResultException e) {
		}
		return null;
	}
	
	protected Predicate appendAndPredicate(Predicate predicate, Predicate appendPred) {
		if(predicate == null){
			return appendPred;
		}
		return getEntityManager().getCriteriaBuilder().and(predicate, appendPred);
	}

	protected abstract Class<T> getEntityClazz();
	protected abstract String getCountNamedQuery();
	protected abstract String getListNamedQuery();
	protected abstract Expression<Boolean> createWhereFromSearchFilter(SearchFilter searchFilter, Root<T> root);
	protected abstract String getFindByNameNamedQuery();

}